Self-hosting with Podman
The office install is HTTPS on port 47391 at https://app.socialautomagic.com:47391. A server that already has a hostname in deploy/local.env keeps it. Rendering a new env file, with that variable unset, uses app.socialautomagic.com. You can run the same shape on your own server. Postgres is not opened on the host. The site asks for a sign-in. There is no guest view of customers, posts, or connected accounts.
Nothing in this guide connects a live social account for you. You run the commands on the server.
What starts
Section titled “What starts”One image runs four commands: migrate (applies database changes, then exits), api (the website, the API, and the MCP address, on port 8787 inside the private network), worker (sends approved posts when a network is configured), and bootstrap (creates the first owner, once, then exits).
Caddy is a separate small container. It is the only program that listens on the public port. It presents the certificate and passes traffic to the API.
| Name | What it is |
|---|---|
| Port 47391 | The only new port |
Network automagic |
Private network for these containers |
Volume automagic-pg |
The database files |
Volume automagic-media |
Uploaded pictures and video |
Volume automagic-caddy |
The certificate authority. Keep it. If you delete it, every computer must trust a new certificate. |
deploy/up.sh starts this stack. deploy/quadlet/install.sh installs systemd units so it comes back after a reboot. deploy/podman-compose.yml describes the same containers if you already use compose. Use one of those, not two at the same time.
Before you start
Section titled “Before you start”- A normal user on the server, not root. Podman runs as that user.
loginctl enable-lingerfor that user, so the site stays up after they log out.openssl, to create passwords.- Port 47391 free. Do not take 80, 443, 3000, 5432, or 8080 if something else already uses them.
From the repository directory, render the env files. They are readable only by this user. Do not commit them.
chmod +x deploy/*.sh deploy/quadlet/install.shdeploy/render-env.shThen set AUTOMAGIC_LAN_IP to the server’s address on the local network, such as 192.168.1.20. That example is not your server. Leave BOOTSTRAP_PASSWORD empty so the first start writes a long random password to deploy/.owner-credentials and does not print it. Set BOOTSTRAP_EMAIL only if you want an address other than [email protected].
Leave the YouTube, TikTok, X, Instagram, LinkedIn, and blog secrets empty until those apps exist. Leave X_HTTP_ALLOW_NETWORK unset until you mean to call X. Leave TIKTOK_APP_AUDITED unset. Leave MEDIA_PUBLIC_ORIGIN unset until a public https origin can serve media.
deploy/preflight.sh stops if 47391 is taken, or if a container, volume, or network with these names belongs to something else. It does not stop other apps.
Open only 47391/tcp on the firewall. Do not open 5432. On the router, forward TCP 47391 to the server’s LAN address, same port. If you use the name app.socialautomagic.com, leave that DNS record pointed at the server and do not proxy it in a way that hides the certificate this server presents.
Build and start
Section titled “Build and start”podman build --format docker -f Containerfile -t localhost/automagic-social:latest .deploy/up.sh--format docker keeps the image health check. The image runs as a user named automagic, not as root. It reads settings from the environment only. Secrets are not baked into the image. Docker works for the same Containerfile. Set CONTAINER_RUNTIME=docker when you run the scripts.
Do not push the image to a public registry. If you build on another machine, podman save and podman load carry the file across.
After you change the code, rebuild and run deploy/up.sh update. The database container keeps running. deploy/up.sh reset-owner replaces the owner password, writes it to deploy/.owner-credentials, and signs that person out everywhere. The password is not shown on screen.
Certificate
Section titled “Certificate”Caddy’s certificate is issued by its own local authority (tls internal). It is not a public certificate. Browsers warn until each computer trusts that authority. The warning is expected. Do not type the password on a warning page.
The authority covers the public name, the LAN name automagic.lan, and the address in AUTOMAGIC_LAN_IP. Copy the CA out of the Caddy container and trust it on each computer:
podman cp automagic-caddy:/data/caddy/pki/authorities/local/root.crt "$HOME/automagic-root.crt"- Fedora or RHEL: copy it into
/etc/pki/ca-trust/source/anchors/and runupdate-ca-trust. - Ubuntu: copy it into
/usr/local/share/ca-certificates/and runupdate-ca-certificates. - macOS: add it to the System keychain as a trusted root.
- Windows:
certutil -addstore -f "ROOT" automagic-root.crtfrom an administrator prompt. - Firefox keeps its own list unless you import the file or set
security.enterprise_roots.enabledto true.
Check from the server, with the CA file:
curl --cacert "$HOME/automagic-root.crt" \ --resolve app.socialautomagic.com:47391:127.0.0.1 \ https://app.socialautomagic.com:47391/api/healthA good answer is {"ok":true}. deploy/check.sh also checks that Postgres and the API have no host port, and that a visitor who is not signed in is refused by /api/auth/me.
Come back after a reboot
Section titled “Come back after a reboot”loginctl enable-linger "$USER"deploy/quadlet/install.sh --startThe units live in ~/.config/containers/systemd/. Postgres still has no host port.
Backups and health checks
Section titled “Backups and health checks”Nightly backups stay off until the env file names a directory:
AUTOMAGIC_BACKUP_DIR=/var/backups/automagicAUTOMAGIC_BACKUP_KEEP_DAYS=14deploy/up.sh installs a timer when that directory is set. On a root Podman install the timer is a system unit. A rootless user gets a user timer. The job runs pg_dump inside the Postgres container, gzips the database and the media files, drops copies older than the keep window (14 days unless you change it), and writes status.json. That file has no password and no encryption key.
An optional AUTOMAGIC_BACKUP_RCLONE_REMOTE copies the archives off the server with rclone. Leave it empty to keep them only on this machine.
Preview a restore before you replace anything. The dry run does not stop containers:
deploy/restore.sh --dry-run 20261110-023000RESTORE_CONFIRM=yes deploy/restore.sh 20261110-023000The confirmed restore stops the API and the worker, replaces the database and the media volume, and starts them again. Keep the same encryption key. It is not stored in the backup.
A second timer checks /api/health, the worker heartbeat, free disk, and the age of the last backup. Set AUTOMAGIC_ALERT_WEBHOOK for a Slack or Discord compatible JSON alert, and AUTOMAGIC_ALERT_EMAIL to also write the app’s mail outbox. Leave both empty and nothing is sent. A signed-in owner or admin sees the heartbeat and the last backup time on GET /api/health. Everyone else still gets {"ok":true}.
Set the brand plan
Section titled “Set the brand plan”A new workspace starts on Tier 1, which includes 1 brand. Tier 2 includes 5. Tier 3 includes 10. A custom cap is a whole number from 11 to 10000. Customers cannot change this in the app. Lowering the plan does not delete brands or disconnect accounts. Existing brands stay usable. New brands wait until the count fits or you raise the cap.
From a checkout that has the app environment:
npm run plan -- show <workspace-address>npm run plan -- set <workspace-address> tier_2npm run plan -- set <workspace-address> custom 12show prints the address, the plan, and how many brands are in use. On the container image, pass plan and the same arguments, with the API environment and the private network so the database name automagic-postgres resolves:
podman run --rm --network automagic --env-file deploy/local.env \ localhost/automagic-social:latest plan show <workspace-address>The address is the workspace slug. show prints it. Do not commit deploy/local.env.
Connect a network
Section titled “Connect a network”Register the public name, including the port, as the redirect URI. A private LAN address is not a redirect the product will send, and the networks will not accept it. Trust the CA on the computer that clicks Connect before you start. If the browser is still showing a certificate warning, the redirect will not finish.
Some providers reject a certificate that is not from a public authority, or they refuse a port other than 443. Use a temporary tunnel for that connection, then go back to the office address. While that http tunnel mode is on, other computers on the office network cannot open the site, because it listens only on the server itself.
A public address without a port
Section titled “A public address without a port”Leave this unset unless a proxy already serves the site at an https address with no port. The default stays https://app.socialautomagic.com:47391.
Set AUTOMAGIC_PUBLIC_ORIGIN when a Cloudflare Tunnel or other HTTPS proxy fronts the app without a port. Caddy still listens on 47391. Office computers still open https://app.socialautomagic.com:47391. OAuth callbacks, share links, email links, and the agent card use the public address.
AUTOMAGIC_PUBLIC_ORIGIN=https://app.socialautomagic.comAPP_PUBLIC_ORIGIN=https://app.socialautomagic.comAPI_PUBLIC_ORIGIN=https://app.socialautomagic.comMCP_PUBLIC_ORIGIN=https://app.socialautomagic.comMEDIA_PUBLIC_ORIGIN=https://app.socialautomagic.comKeep COOKIE_SECURE=true. Keep AUTOMAGIC_HOST_PORT=47391. Do not move Caddy to port 443, and do not publish Postgres.
deploy/preflight.sh accepts those origins only when AUTOMAGIC_PUBLIC_ORIGIN is the same address. Without that setting, the origins must include the port.
Point the tunnel at https://127.0.0.1:47391 and skip TLS verification on that hop. The office CA is what Caddy presents to the tunnel and to the LAN. The proxy presents the public certificate. Caddy replaces X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host from the connection it accepted. The app trusts those headers in this mode, and it accepts both the public address and the LAN address. The sign-in cookie stays Secure.
A root Podman process, and firewalld not having 47391/tcp open, are reported as Info in this mode. The tunnel does not need that port on the public firewall. Open it when LAN computers still connect to Caddy. Do not open 5432.
Register the redirect URIs on the public address, with no port. Set MEDIA_SIGNING_KEY before a network needs to download a file from MEDIA_PUBLIC_ORIGIN.
Cloudflare Access
Section titled “Cloudflare Access”Leave this off unless Cloudflare Access is already in front of the public name. With it off, every visit uses the email and password.
AUTOMAGIC_AUTH_MODE=cloudflare-accessCF_ACCESS_TEAM_DOMAIN=coulson.cloudflareaccess.comCF_ACCESS_AUD=CF_ACCESS_AUTO_PROVISION=falseCF_ACCESS_AUD is the Application Audience (AUD) tag. In the Cloudflare dashboard, open Zero Trust, then Access controls, then Applications. Choose Edit. Under Additional settings, copy Application Audience (AUD) Tag. The same steps are on Validate JWTs. Do not commit the tag.
The server checks the Access token signature, issuer, audience, and expiry. It does not trust the email header by itself. A matching email opens a normal session. The password form stays for any request that does not carry a valid token, including https://app.socialautomagic.com:47391. Link-in-bio pages at /l/ stay public and do not start a session. Log out also sends the browser to https://<your-team>.cloudflareaccess.com/cdn-cgi/access/logout.
CF_ACCESS_AUTO_PROVISION defaults to false. Set it to true with CF_ACCESS_ALLOWED_DOMAINS=coulsontechnologies.com only when a new Access email should become an editor. That person is not an owner or an admin. Someone who already has an account keeps their role. Leave CF_ACCESS_CERTS_URL unset.
Keep COOKIE_SECURE on behind HTTPS. Set the public origins to the public addresses. Keep the encryption key and the network secrets on the server. Changing the encryption key makes stored tokens unreadable.