Teams running multiple self-hosted applications — Gitea, Outline, Mattermost, Nextcloud — quickly discover that separate user databases become a compliance and operational liability. Users forget which password belongs to which service, admins deal with password resets across five dashboards, and offboarding a departing employee means hunting through every app manually. Casdoor is an open-source, developer-first identity platform that provides a single authentication layer for all your services. It supports OAuth 2.0, OIDC, SAML 2.0, LDAP, and dozens of social providers — giving every app in your stack one consistent login screen without paying for Okta or Auth0.
This guide covers a production-ready Casdoor deployment on Ubuntu with Docker Compose, PostgreSQL as the backend database, and Caddy handling TLS termination and reverse-proxying. You will finish with a running Casdoor instance, a configured organization, and a working OAuth 2.0 application registration — ready to accept login requests from other services on your stack.
Architecture and flow overview
Casdoor is a Go application that runs a single binary exposing an HTTP API and a React-based admin console. All identity data — users, roles, organizations, applications, and provider configs — lives in PostgreSQL. Casdoor does not bundle its own database engine; it connects to any external PostgreSQL instance, which makes it straightforward to back up, replicate, and migrate independently of the application layer.
In this stack:
- Casdoor container — the Go identity server, exposed only on a local port (
127.0.0.1:8000). - PostgreSQL container — the persistence layer, accessible only on the internal Docker network (
casdoor_net). - Caddy — terminates TLS with an automatic Let's Encrypt certificate, reverse-proxies all HTTP traffic to Casdoor, and enforces HTTPS redirects.
- Docker Compose — orchestrates both containers, manages the shared network, restart policies, and environment injection.
When a user logs in to an application registered with Casdoor, the OAuth 2.0 authorization code flow proceeds like this: the downstream app redirects to Casdoor, Casdoor authenticates the user (local password, LDAP, or social), and returns an authorization code. The app exchanges the code for an access token and ID token using Casdoor's OIDC token endpoint. Casdoor signs tokens with an RSA key it generates at first boot; client apps verify signatures using Casdoor's JWKS endpoint.
Caddy is an ideal front-end here because Casdoor's admin console and API share the same port — Caddy proxies both under the same hostname with no extra routing rules. The reverse_proxy directive handles all traffic uniformly.
Prerequisites
- Ubuntu 22.04 or 24.04 server with a public IP or reachable hostname.
- Docker Engine 24+ and Docker Compose v2 installed (
docker composecommand available). - A DNS A record pointing your desired domain (e.g.,
auth.example.com) to your server IP. - Ports 80 and 443 open in UFW and any upstream firewall.
- Root or sudo access on the server.
- At least 1 GB RAM — Casdoor itself is lightweight but PostgreSQL benefits from a small shared-buffers allocation.
Step-by-step deployment
1. Create the project directory
mkdir -p /opt/casdoor && cd /opt/casdoor2. Create the environment file
Store all secrets in a .env file and restrict its permissions immediately. The Compose file reads these variables via substitution so no credentials drift into version-controlled files.
cat > /opt/casdoor/.env << 'EOF'
CASDOOR_DOMAIN=auth.example.com
CASDOOR_PORT=8000
POSTGRES_DB=casdoor
POSTGRES_USER=casdoor
POSTGRES_PASSWORD=change_me_strong_password_here
EOF
chmod 600 /opt/casdoor/.envReplace auth.example.com with your actual domain and set a strong database password before proceeding.
3. Write the Docker Compose file
version: "3.8"
services:
postgres:
image: postgres:15-alpine
container_name: casdoor_postgres
env_file: .env
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
casdoor:
image: casbin/casdoor:latest
container_name: casdoor
env_file: .env
environment:
driverName: postgres
dataSourceName: "user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} host=postgres port=5432 dbname=${POSTGRES_DB} sslmode=disable"
runmode: prod
httpport: 8000
appname: casdoor
ports:
- "127.0.0.1:${CASDOOR_PORT}:8000"
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8000/api/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
postgres_data:
networks:
default:
name: casdoor_net4. Write the Caddyfile
auth.example.com {
reverse_proxy 127.0.0.1:8000
encode gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
}
log {
output file /var/log/caddy/casdoor.log
format json
}
}Replace auth.example.com with your domain. Caddy will automatically obtain and renew a Let's Encrypt certificate. Note that Casdoor's admin UI uses frames for certain widgets, so X-Frame-Options is set to SAMEORIGIN rather than DENY.
5. Deploy the stack
cd /opt/casdoor
docker compose up -d
docker compose logs -f casdoorWait until you see a log line indicating the HTTP server is listening on port 8000. PostgreSQL will initialize first; Casdoor waits for the healthcheck to pass before starting.
6. Reload Caddy
systemctl reload caddy7. Configure the systemd override for Docker Compose
Create a systemd service so the stack starts automatically after reboots and Docker daemon restarts.
cat > /etc/systemd/system/casdoor.service << 'EOF'
[Unit]
Description=Casdoor Identity Server
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/casdoor
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable casdoor
systemctl start casdoorConfiguration and secrets handling
The Casdoor default admin credentials are admin / 123. Change these immediately after first login via Admin > Users > admin > Edit. Then configure your first Organization (the logical tenant that groups users and applications):
- Navigate to Organizations and edit the built-in
built-inorganization or create a new one named after your company. - Under Applications, click Add to register your first downstream service. Set the Redirect URIs to the callback URL of the application (e.g.,
https://gitea.example.com/user/oauth2/casdoor/callback). - Copy the Client ID and Client Secret shown in the application record — paste these into your downstream app's OAuth config.
For the database password, never commit the .env file to version control. On production servers, consider mounting the secrets from a secrets manager (Infisical, Vault) or using Docker secrets with Swarm mode. At minimum:
# Verify .env permissions are restrictive
stat -c "%a %U" /opt/casdoor/.env
# Expected: 600 rootCasdoor stores its JWT signing key in the database. Back up the PostgreSQL volume regularly so you can restore the complete identity configuration, including all registered applications and user accounts, from a single snapshot.
Verification
Run these checks after deployment to confirm everything is healthy before pointing downstream apps at Casdoor:
# Check container health
docker compose -f /opt/casdoor/docker-compose.yml ps
# Confirm Casdoor is listening locally
curl -sf http://127.0.0.1:8000/api/health && echo "Casdoor healthy"
# Confirm HTTPS is reachable through Caddy
curl -sf https://auth.example.com/api/health && echo "Caddy proxy healthy"
# Confirm OIDC discovery endpoint is available
curl -s https://auth.example.com/.well-known/openid-configuration | python3 -m json.tool | grep issuer
# Check database connectivity
docker exec casdoor_postgres pg_isready -U casdoor -d casdoorThe OIDC discovery endpoint should return a JSON document whose issuer matches your Casdoor domain. Downstream apps will fetch this document automatically to learn token and JWKS endpoints.
Common issues and fixes
Casdoor container exits immediately after start. The most common cause is a database connection error. Check docker compose logs casdoor for a message like failed to connect to PostgreSQL. Verify the POSTGRES_PASSWORD variable in .env matches what PostgreSQL was initialized with. If you changed the password after initial creation, the existing data volume retains the old password — drop and recreate the volume or use ALTER ROLE casdoor PASSWORD 'newpass' inside the container.
Admin console unreachable at https://auth.example.com. Confirm that Caddy obtained a certificate by running journalctl -u caddy --no-pager | grep "certificate". If Caddy failed, verify DNS is pointing to your server IP and ports 80/443 are open in UFW. Also check curl http://127.0.0.1:8000 — if Casdoor is running but Caddy is not reachable, the issue is upstream.
OAuth callback fails with "redirect_uri mismatch". Open the Application record in Casdoor and confirm the exact Redirect URI matches what the client app sends, including trailing slashes and protocol (https://). Casdoor performs strict URI matching.
JWT tokens fail signature verification in downstream apps. This typically means the downstream app fetched the JWKS before Casdoor generated its key pair. Restart the downstream app or force it to refresh the JWKS cache. Casdoor generates the RSA key on first boot and stores it in the database; it does not rotate automatically unless you trigger a key rotation from the admin UI.
PostgreSQL volume data persists old schema after a major version upgrade. If you upgrade from Casdoor 1.x to a major version with schema migrations, let the container run and watch logs for auto-migration output. Casdoor uses XORM and migrates the schema on startup. Back up the volume before any major version upgrade.
FAQ
Can Casdoor act as an identity broker for Google, GitHub, or Microsoft logins?
Yes. Casdoor supports social login providers natively. In the admin console, navigate to Providers, click Add, and select your provider (Google, GitHub, Microsoft, GitLab, and many more). You will need to create an OAuth application on the provider's developer portal, then paste the Client ID and Secret into Casdoor. Once configured, the login screen for your organization will display the provider's button automatically.
How do I connect Gitea to Casdoor for SSO?
In Casdoor, create an Application with a Redirect URI of https://gitea.example.com/user/oauth2/casdoor/callback and note the Client ID and Client Secret. In Gitea, go to Site Administration > Authentication Sources > Add Authentication Source, select OAuth2, choose the OpenID Connect provider, and fill in the Auto Discovery URL: https://auth.example.com/.well-known/openid-configuration. Users who sign in via Casdoor will be provisioned automatically in Gitea.
Does Casdoor support multi-factor authentication?
Yes. Casdoor supports TOTP-based MFA (Google Authenticator, Authy) and SMS/email one-time codes for supported providers. Enable it per-organization under Organizations > Edit > MFA. Users are prompted to enroll during their next login if MFA is set to required for the organization. App-specific passwords for non-OIDC clients (e.g., IMAP or SMTP applications) can be managed through Casdoor's user portal.
How do I back up and restore Casdoor?
All state lives in PostgreSQL. Back up the volume with:
docker exec casdoor_postgres pg_dump -U casdoor casdoor | gzip > /backup/casdoor_$(date +%Y%m%d).sql.gzTo restore, create a fresh PostgreSQL container, copy the dump in, and run psql -U casdoor casdoor < backup.sql before starting the Casdoor container. The JWT signing key is embedded in the database export, so all registered applications and tokens continue to work after restore without re-registering clients.
Can I run Casdoor behind Cloudflare?
Yes, with one caveat: set Cloudflare SSL/TLS mode to Full (strict) so Cloudflare validates Caddy's Let's Encrypt certificate. Do not use Flexible mode, which sends plaintext HTTP to your origin. Casdoor's OIDC redirect URIs must use the https:// scheme; Cloudflare's proxied edge satisfies this. If you use Cloudflare Tunnel instead of direct proxying, replace the Caddy listener with the cloudflared ingress config and remove the public port bindings from the Compose file.
How do I update Casdoor to a new version?
cd /opt/casdoor
docker compose pull
docker compose up -d --remove-orphansCasdoor applies schema migrations automatically on startup. Always back up the PostgreSQL volume before pulling a major version upgrade. Check the Casdoor GitHub release notes for breaking changes in application configuration or token formats before upgrading production instances.
Internal links
- Production Guide: Deploy Authentik with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Vaultwarden with Docker Compose + Caddy on Ubuntu
- Production Guide: Deploy Gitea with Docker Compose + Caddy + PostgreSQL on Ubuntu
Talk to us
If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.