> This page location: Docs > Self-Hosting > Going Live on Your Own Server
> Full index for agents: https://www.semantius.com/llms.txt

# Going Live on Your Own Server

> Move a self-hosted Semantius stack from your laptop to a real server, with your own domain and HTTPS.

- **URL**: https://www.semantius.com/docs/self-hosted/deploy

---

A local install and a real deployment run the same stack. What changes is that the stack now has a public address, so a handful of settings that pointed at `localhost` have to point at your domain instead, and the placeholder secrets have to be real.

This page covers a plain server you administer yourself: a VPS, a cloud instance, a machine in your office. If you run [Dokploy](https://www.semantius.com/docs/self-hosted/dokploy.md), use that page instead, because the blueprint handles most of this for you.

**This page is written for the `local-semantius-idp` variant.** It works for `local-entra-idp` too, with the differences listed under [Using the Entra Variant](#using-the-entra-variant). [Choose or Build Your Variant](https://www.semantius.com/docs/self-hosted/variants.md) lists every variant and which pages apply to each.

## Before You Start

You need a server with Docker installed, a domain name, and a DNS record pointing that domain at the server's IP address. Set the DNS record up first and let it propagate. Certificates are issued by proving you control the domain, so nothing below works until the name resolves.

Then get the files onto the server and move into the variant folder, exactly as you would locally:

```bash
git clone https://github.com/semantius/semantius-self-hosted
```

```bash
cd semantius-self-hosted/variants/local-semantius-idp
```

```bash
./setup-env.sh
```

Do not copy a `.env` from your laptop. The generated secrets are meant to be unique per installation, and the values in it are tied to the database that laptop created.

## The Settings That Have To Change

Open `.env` and set these four. Most of the rest of the file can stay at its default for a first deployment.

| Setting             | Set it to                             | Why                                                                         |
| ------------------- | ------------------------------------- | --------------------------------------------------------------------------- |
| `SITE_ADDRESS`      | `yourdomain.com`                      | Tells the front door which name it is serving, and turns on automatic HTTPS |
| `IDP_BASE_URL`      | `https://yourdomain.com/idp`          | The address the sign-in system considers its own, including the `/idp` part |
| `PUBLIC_WEB_ORIGIN` | `https://yourdomain.com`              | Where the app lives, used to check that sign-ins return to the right place  |
| `PUBLIC_API_URL`    | `https://yourdomain.com/gateway/rest` | The address the API documentation tells people to call                      |

`IDP_BASE_URL` keeps the `/idp` suffix and `PUBLIC_WEB_ORIGIN` has no path at all. That difference is not a typo. The first is the sign-in system's own address, the second is the app's.

**Then decide about the sample data.** `NWIND=TRUE` is on by default and loads the Northwind demo dataset into your database. Remove that line unless you want demo customers and orders in production. It has to happen before your first `./create.sh`, because sample data is loaded only when the database is created.

Also confirm your three secrets are the generated ones and not the placeholders from the example file. `setup-env` handles this, but it is worth a look before a machine goes on the public internet:

```bash
grep -E 'IDP_SECRET|POSTGRES_PASSWORD|SEMANTIUS_AUTHENTICATOR_PASSWORD' .env
```

If any of them still reads `dev-only-...`, `postgres`, or `devpassword`, stop and fix that first. `IDP_SECRET` in particular has to be right **before the first start**: it encrypts the keys that sign everyone's logins, so changing it later signs everyone out and makes the stored keys unreadable.

## Keep Your Changes in an Override File

Two of the steps below change which ports the stack publishes. Make those changes in a file of your own called `docker-compose.override.yml`, next to `docker-compose.yml`, rather than in `docker-compose.yml` itself.

Docker Compose reads the override file automatically, so every script picks it up. It is not part of the repository, so updating the stack files with `git pull` leaves it alone. An edit to `docker-compose.yml` would instead block the next update with a conflict. [Choose or Build Your Variant](https://www.semantius.com/docs/self-hosted/variants.md#customizing-a-variant) explains the approach in full, and the alternative of working in a copy of the variant folder.

The file uses the `!override` tag, which needs Docker Compose 2.24.4 or newer. Check with `docker compose version`.

## Turning On HTTPS

Setting `SITE_ADDRESS` to a bare domain makes the front door request a certificate automatically, for free, and renew it on its own. For that to work it has to be reachable on the standard web ports, which means publishing ports 80 and 443 rather than the local development port.

Create `docker-compose.override.yml` in the variant folder with this content:

```yaml
services:
  semantius:
    ports: !override
      - "80:80"
      - "443:443"
```

The `!override` tag replaces the published ports instead of adding to them, so port 3000 is no longer published.

Make sure your server's firewall allows both. Port 80 is not optional even though nothing is served on it: it is how the certificate authority checks that you control the domain, and how visitors who type a bare domain get redirected to the secure version.

## Restrict the Database Ports

The stack publishes two database ports on purpose. Port 6432 is the pooled connection for your own applications that talk SQL, and port 5434 is the direct connection for administration and for migrations with the `pg_semantius` CLI.

**They are published on every network interface, so on a public server they are reachable from the internet.** They are then protected by a password alone, and 5434 accepts the all-powerful `postgres` user. On Linux, a firewall such as `ufw` does not help here, because ports Docker publishes bypass it.

If no other machine needs SQL access, bind both to the server itself. If some do, allow only those machines, for example in your cloud provider's firewall. Add the `postgres` and `pgbouncer` services to the same override file, which then reads in full:

```yaml
services:
  semantius:
    ports: !override
      - "80:80"
      - "443:443"
  postgres:
    ports: !override
      - "127.0.0.1:${POSTGRES_PORT:-5434}:5432"
  pgbouncer:
    ports: !override
      - "127.0.0.1:${PGBOUNCER_PORT:-6432}:5432"
```

Nothing inside the stack uses those published ports, so the app and the API keep working. Applications and the `pg_semantius` CLI on the server itself still connect as before, and from your own machine they can connect through an SSH tunnel.

## Pin Your Versions

By default the stack downloads the newest version of every piece each time you run `./up.sh` or `./create.sh`. A plain restart or a reboot does not download anything. But on a server you depend on, the next routine `./up.sh` after a settings change would also move you to whatever was published since. Pin the three Semantius pieces in `.env` before you go live:

```bash
SEMANTIUS_DB_VERSION=x.y.z-pg18
SEMANTIUS_APP_VERSION=x.y.z
SEMANTIUS_IDP_VERSION=x.y.z
```

Replace each `x.y.z` with a real version from the image's package page: [database](https://github.com/orgs/semantius/packages/container/package/postgres), [web app](https://github.com/orgs/semantius/packages/container/package/semantius-app), [sign-in service](https://github.com/orgs/semantius/packages/container/package/semantius-idp). Updating then becomes something you do deliberately: change the tag, run `./up.sh`, and roll back by changing it again.

The other pieces, such as the API server and the front door, cannot be pinned from `.env` yet. [Running Your Stack](https://www.semantius.com/docs/self-hosted/running-your-stack.md#choosing-a-version) shows how to hold them back with the override file.

## Turn On Email

Without an email service the sign-in system runs in a reduced mode: no password resets, no address verification, no notifications. That is fine on a laptop where an administrator creates every account by hand. On a real deployment people will lock themselves out.

Add a [Resend](https://resend.com) API key to `.env`:

```bash
RESEND_API_KEY=re_your_key_here
```

The features switch on together the next time the stack starts.

## Start It

```bash
./create.sh
```

Use `create` for a first deployment, because there is no data to lose and you want the database built from scratch. On every later change, use `./up.sh`, which applies the new settings and keeps your data.

Every long-running service is set to restart on its own, so the stack comes back after a crash or a server reboot without anyone logging in. Once it holds real data, set up the daily backup described in [Running Your Stack](https://www.semantius.com/docs/self-hosted/running-your-stack.md#backing-up).

Then open `https://yourdomain.com` and complete the setup page. **The first person to finish it becomes the administrator**, so do this immediately. Until somebody does, that page is open to anyone who finds the address.

## Checking It Worked

```bash
curl https://yourdomain.com/.well-known/semantius.json
```

Every Semantius deployment publishes a short description of itself at that address. The values in it should carry your domain rather than `localhost`, which confirms the front door picked up the new settings. If they are empty or still local, the containers were restarted rather than recreated. Run `./up.sh`, which recreates them properly.

## Using the Entra Variant

Going live with `local-entra-idp` follows the same steps, in its own folder, with three differences:

- **Set `SITE_ADDRESS` and `PUBLIC_API_URL`.** Here the API address is `https://yourdomain.com/rest`, the API itself, because API keys and the `/gateway/` address are part of semantius-idp. `IDP_BASE_URL`, `PUBLIC_WEB_ORIGIN` and the email section configure semantius-idp too, so they do not apply. Sign-in, password resets and account email are Entra's.
- **Update the address registered in Entra.** The web app's redirect address has to match the new domain exactly, or sign-in fails with `AADSTS50011`. Add `https://yourdomain.com/oauth2_callback` under the **Single-page application** platform of the web app registration.
- **Schedule the key refresh.** Run `./jwks-refresh.sh` daily, as described on the [Entra page](https://www.semantius.com/docs/self-hosted/entra-id.md#keeping-keys-fresh).

## A Note on Serving Several Domains

There is a setting called `IDP_DYNAMIC_ISSUER` that makes the stack follow whatever domain a request arrives on. It is useful, and it is **not** safe in the setup described on this page, because a front door configured with one domain still answers to any name pointed at the server. Leave it off here. It is designed for deployments that sit behind a router which only forwards known domains, which is exactly what [Dokploy](https://www.semantius.com/docs/self-hosted/dokploy.md) provides.

## Next Steps

- **[Running Your Stack](https://www.semantius.com/docs/self-hosted/running-your-stack.md)**: the everyday commands, backups, and how to update.
- **[Settings Reference](https://www.semantius.com/docs/self-hosted/settings.md)**: every value in `.env`, grouped by what it does.
- **[Dokploy](https://www.semantius.com/docs/self-hosted/dokploy.md)**: if you would rather not administer a server by hand.

---

## Related

- [Running Your Stack](https://www.semantius.com/docs/self-hosted/running-your-stack.md)
- [Dokploy](https://www.semantius.com/docs/self-hosted/dokploy.md)
- [Docs home](https://www.semantius.com/docs.md)
- [Full index for agents](https://www.semantius.com/llms.txt)
