Deploy with Docker
Raytha is one container plus PostgreSQL 17. This page covers the Compose file in the repository, a production-shaped variant with automatic HTTPS, and how to update and troubleshoot it.
Tip If you do not want to run servers, Deploy with Railway is the shortest path. Everything on this page applies to any host that runs Docker.
What you are running
- The image is published on Docker Hub as
raythahq/raytha. Every release is tagged with its version (raythahq/raytha:2.0.0) andlatestpoints at the newest release. Pin a version in production and move the tag deliberately when you upgrade. - It is built from the
Dockerfilein the repository (.NET 10 runtime, working directory/app), so you can also build it yourself withdocker build -t raytha .if you need a patched image. The app listens on port8080inside the container. - The image sets
AdminSpa__AutoStart=false: the admin app is a prebuilt bundle served by the .NET process, so there is no Node process in the container. - The image declares a health check,
curl -fsS http://127.0.0.1:8080/healthz, every 30 seconds. See Health checks.
Run the published image
The quickest way to try Raytha is the one-file Compose setup in the quickstart, which pulls raythahq/raytha and runs PostgreSQL next to it. Without Compose, the same thing is a single command once you have a PostgreSQL database:
docker run -d --name raytha -p 8080:8080 \
-e ConnectionStrings__DefaultConnection="Host=db.example.com;Port=5432;Username=raytha;Password=...;Database=raytha" \
-e APPLY_PENDING_MIGRATIONS=true \
-v raytha_user_uploads:/app/user-uploads \
raythahq/raytha:2.0.0
The Compose file in the repository
If you clone the repository, its docker-compose.yml builds the image from source instead of pulling it. That is what you want when developing Raytha itself; for running it, prefer the published image.
git clone https://github.com/RaythaHQ/raytha.git
cd raytha
cp .env.example .env
docker compose --env-file .env up -d --build
It starts two services and two named volumes:
| Item | Details |
|---|---|
app | Built from ., published on host port 5001 (container 8080). Waits for the database to be healthy. |
db | postgres:17, published on host port 5433, user postgres, password changeme, database raytha. |
raytha_pg_data | PostgreSQL data directory. |
raytha_user_uploads | Mounted at /app/user-uploads, where the Local file storage provider writes. See File storage. |
Every variable the app reads is passed through from .env with a default, so you configure Raytha by editing .env and running docker compose --env-file .env up -d again. The full list is in Configuration.
Two defaults make this file a poor production base:
ASPNETCORE_ENVIRONMENTdefaults toDevelopment, so cookies are not markedSecureand HTTPS is not enforced.- The database password is hard-coded as
changeme, and PostgreSQL is published on the host.
A production setup
Save this as docker-compose.prod.yml in any directory. It pulls the published image, keeps PostgreSQL private, takes the password from .env, and puts Caddy in front for HTTPS and certificates.
services:
app:
image: raythahq/raytha:2.0.0
restart: unless-stopped
environment:
ASPNETCORE_ENVIRONMENT: Production
ConnectionStrings__DefaultConnection: Host=db;Port=5432;Username=raytha;Password=${DB_PASSWORD:?set DB_PASSWORD in .env};Database=raytha
APPLY_PENDING_MIGRATIONS: "true"
TRUSTED_PROXIES: private
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USERNAME: ${SMTP_USERNAME:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_FROM_ADDRESS: ${SMTP_FROM_ADDRESS:-}
volumes:
- raytha_user_uploads:/app/user-uploads
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: raytha
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
POSTGRES_DB: raytha
volumes:
- raytha_pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U raytha"]
interval: 5s
timeout: 5s
retries: 5
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
depends_on:
- app
volumes:
raytha_pg_data:
raytha_user_uploads:
caddy_data:
Next to it, create the Caddyfile (use your own host name) and a .env with the database password:
raytha.example.com {
reverse_proxy app:8080
}
echo "DB_PASSWORD=$(openssl rand -hex 24)" > .env
docker compose -f docker-compose.prod.yml up -d
Point the DNS record for raytha.example.com at the server first, so Caddy can obtain a certificate. Then open https://raytha.example.com and finish the setup screen. Set Website URL to https://raytha.example.com.
Why these settings
ASPNETCORE_ENVIRONMENT=Productionturns on HTTPS redirection, HSTS andSecurecookies. Caddy sendsX-Forwarded-Proto: https, which Raytha honours, so there is no redirect loop.TRUSTED_PROXIES=privatetrusts forwarded headers only from private addresses, which covers the Docker network that Caddy and the app share. The app has no published port, so nothing else can reach it. The default,all, trusts any caller. See Running behind a proxy.APPLY_PENDING_MIGRATIONS=trueapplies EF Core migrations when the container starts, which is what you want when you move to a newer image tag.- The password in the connection string cannot contain
;. A hex string fromopenssl rand -hexis safe.
Day-two commands
# Follow logs
docker compose -f docker-compose.prod.yml logs -f app
# Is it healthy?
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml exec app curl -s http://127.0.0.1:8080/healthz/ready
# Stop (data stays in the volumes)
docker compose -f docker-compose.prod.yml down
Do not add -v to down unless you intend to delete the database and the uploads.
Update to a newer version
- Back up the database and the uploads.
- Check what changed. A minor-version bump (2.0.x to 2.1.0) means the release adds a database migration.
- Change the image tag in
docker-compose.prod.ymlto the new version, then pull and restart (commands below). - Watch the app log until it reports that it started, then open
/healthz/ready.
docker compose -f docker-compose.prod.yml pull app
docker compose -f docker-compose.prod.yml up -d app
Migrations run in the new container on start-up. Going back to an older image does not undo them; to roll back, restore the backup you took in step 1.
Gotchas
- Port 5433 collision. The repository's compose file publishes PostgreSQL on host port
5433. The development compose file used by local development uses the same port. Run only one of them, or change the host side of the mapping. - Changing the database password later. PostgreSQL reads
POSTGRES_PASSWORDonly when it initialises an empty data directory. Changing it afterwards does not change the existing user's password. - Local uploads need the volume. Without
raytha_user_uploads(or a cloud provider), files in the media library disappear with the container. - Plain HTTP in Production. If you publish the app port directly and visit it over HTTP with
ASPNETCORE_ENVIRONMENT=Production, the sign-in cookie isSecureand browsers will not keep it on a non-localhost HTTP address. Put a TLS-terminating proxy in front, or useDevelopmentfor a local trial only. - Mounting under a sub-path.
PATHBASEexists for this. In 2.0.0 the admin bundle still references its assets at/raytha/...without the prefix, so test the admin before you rely on it.
Next steps
- Configuration: every environment variable.
- Running behind a proxy: forwarded headers, Cloudflare and the Website URL.
- Backups and restores and Health checks.