Skip to content

Deploy Team Server with Podman Compose and a local PostgreSQL container, or run it rootless against an existing PostgreSQL service.

Prerequisites

  • Podman 4.4 or later
  • A Compose provider for the Compose method
  • PostgreSQL 18 or a compatible managed PostgreSQL service
  • At least 2 GB of memory and 10 GB of disk for Team Server
  • A Team Server license and an OIDC application for production authentication
  • A public HTTPS URL reachable by users and Runtime Sensors

For rootless services that must survive logout and reboot, enable lingering:

sudo loginctl enable-linger "$USER"

Prepare Configuration

mkdir -p team-server/config
cd team-server

Save the production configuration example as config/production.yaml. Before deployment:

  1. Add the discovery jobs for the CI/CD providers you use.
  2. Configure at least one OIDC provider.
  3. Add outbound HTTP policy for private identity providers, integration endpoints, or internal certificate authorities.
  4. Confirm that server.host, CORS, and settings.tenant.base_url use SERVER_URL.

Generate a credential-encryption key, two distinct JWT secrets, and a database password:

openssl rand -base64 32
openssl rand -base64 48
openssl rand -base64 48
openssl rand -hex 24

Create .env:

POSTGRES_DB=team_server
POSTGRES_USER=team_server
POSTGRES_PASSWORD=<generated_database_password>
DATABASE_URL=postgresql://team_server:<generated_database_password>@postgres:5432/team_server

ENDURA_CREDENTIAL_ENCRYPTION_KEY=<credential_encryption_key>
ENDURA_LICENSE_KEY=<license_key>
JWT_SENSOR_SECRET=<first_base64_secret>
JWT_USER_SECRET=<second_base64_secret>
SERVER_URL=https://team-server.example.com
TENANT_NAME=Example Organization

GOOGLE_OIDC_CLIENT_ID=<client_id>
GOOGLE_OIDC_CLIENT_SECRET=<client_secret>

Use the variables referenced by your selected OIDC provider, then protect the file:

chmod 600 .env
printf '%s\n' '.env' >> .gitignore

Startup requires a 32-byte encryption key and two different JWT secrets of at least 32 bytes each.

Method 1: Podman Compose

Create podman-compose.yaml:

services:
  postgres:
    image: docker.io/postgres:18-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  endura-team-server:
    image: ghcr.io/endurasecurity/container/endura-team-server:testing
    env_file: .env
    ports:
      - "5150:5150"
    volumes:
      - ./config:/app/config:ro,Z
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:5150/_readiness"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    restart: unless-stopped

volumes:
  postgres_data:

Start and verify the services:

podman compose -f podman-compose.yaml up -d
podman compose -f podman-compose.yaml logs -f endura-team-server
podman compose -f podman-compose.yaml ps
curl -f http://localhost:5150/_readiness

Readiness returns HTTP 200 with {"ok":true} after startup completes.

Method 2: Existing PostgreSQL

Update DATABASE_URL in .env with the external database host, then run Team Server rootless:

podman run -d \
  --name endura-team-server \
  --userns=keep-id \
  --env-file .env \
  -p 5150:5150 \
  -v "$PWD/config:/app/config:ro,Z" \
  --restart unless-stopped \
  ghcr.io/endurasecurity/container/endura-team-server:testing

Verify startup:

podman ps --filter name=endura-team-server
podman logs endura-team-server
curl -f http://localhost:5150/_readiness

Run Direct Podman with Quadlet

Quadlet gives a rootless direct deployment a persistent user service. Use absolute paths for the environment file and configuration directory.

Create ~/.config/containers/systemd/endura-team-server.container:

[Unit]
Description=Endura Team Server
Wants=network-online.target
After=network-online.target

[Container]
Image=ghcr.io/endurasecurity/container/endura-team-server:testing
ContainerName=endura-team-server
EnvironmentFile=/absolute/path/to/team-server/.env
Volume=/absolute/path/to/team-server/config:/app/config:ro,Z
PublishPort=5150:5150
UserNS=keep-id

[Service]
Restart=always

[Install]
WantedBy=default.target

Stop a manually started container with the same name gracefully, remove it, then start the service:

podman stop --time 60 endura-team-server
podman rm endura-team-server
systemctl --user daemon-reload
systemctl --user start endura-team-server.service
systemctl --user status endura-team-server.service

The [Install] section activates the generated service at user-manager startup. Quadlet services are generated units; do not run systemctl enable on them. Lingering starts the user manager at boot.

View service logs with journalctl --user -u endura-team-server.service -f.

Configure HTTPS

By default, forward public HTTPS through a trusted proxy to HTTP port 5150. Set SERVER_URL to the public HTTPS origin; keep the internal HTTP readiness check.

Direct TLS Termination

To terminate TLS in Team Server instead, configure:

settings:
  tls:
    certificate: "/run/secrets/server_cert"
    private_key: "/run/secrets/server_key"

Mount the certificate and key into the Team Server container:

-v /absolute/path/server.pem:/run/secrets/server_cert:ro,Z
-v /absolute/path/server-key.pem:/run/secrets/server_key:ro,Z

Add the mounts under volumes for Compose or as Volume= entries for Quadlet.

Direct TLS replaces HTTP on port 5150. Set SERVER_URL=https://team-server.example.com:5150, with matching OIDC callbacks, CORS, and tenant URL. If a front end maps public 443 to this listener, use its origin instead.

Remove the HTTP-only Compose healthcheck. Monitor public HTTPS /_readiness with certificate validation and use HTTPS for subsequent checks.

Recreate the Compose container after changing .env, certificate mounts, and configuration. For Quadlet, reload the edited container definition before restarting:

systemctl --user daemon-reload
systemctl --user restart endura-team-server.service

Verify the public endpoint:

curl -f https://team-server.example.com:5150/_readiness

Use the port-443 URL if applicable. For a private CA, pass --cacert /path/to/ca.pem; reserve -k for temporary self-signed testing.

Set Up the First Administrator

Sign in to create the unscoped Viewer record, then find it by OIDC email or built-in <ENDURA_AUTH_USERNAME>@localhost (for example, developer@localhost):

podman compose -f podman-compose.yaml exec endura-team-server \
  endura task user_get email:admin@example.com

For direct Podman, replace the Compose prefix with podman exec endura-team-server.

Read the numeric ID from the response’s data, then assign the Administrator role:

podman compose -f podman-compose.yaml exec endura-team-server \
  endura task user_update id:42 role:administrator

Refresh the browser or sign in again.

Operate the Deployment

Logs and Status

podman compose -f podman-compose.yaml ps
podman compose -f podman-compose.yaml logs -f endura-team-server
podman compose -f podman-compose.yaml logs -f postgres

For Quadlet, use systemctl --user status endura-team-server and journalctl --user -u endura-team-server -f.

Update

Review Runtime Sensor compatibility before a major update.

Before upgrading, set ENDURA_CREDENTIAL_ENCRYPTION_KEY and check the plaintext credential blocker; adding a key does not convert old credentials.

Podman Compose:

podman compose -f podman-compose.yaml pull endura-team-server
podman compose -f podman-compose.yaml up -d endura-team-server

Quadlet:

podman pull ghcr.io/endurasecurity/container/endura-team-server:testing
systemctl --user restart endura-team-server.service

To rotate the credential-encryption key for either deployment method:

  1. Generate a new value with openssl rand -base64 32.
  2. In .env, move the old value to ENDURA_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS and set ENDURA_CREDENTIAL_ENCRYPTION_KEY to the new value.
  3. Apply the changed environment file with podman compose -f podman-compose.yaml up -d endura-team-server or systemctl --user restart endura-team-server.service for Quadlet.
  4. Confirm that the logs report credentials validated and that /_readiness succeeds.
  5. Remove ENDURA_CREDENTIAL_ENCRYPTION_PREVIOUS_KEYS, restart again, and repeat the validation checks.

Keep the previous key until restart succeeds. JWT rotation separately revokes tokens signed with the old secret.

Back Up and Restore

podman compose -f podman-compose.yaml exec -T postgres \
  sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > team_server.sql
tar -czf team_server_config.tar.gz config/

Back up source secrets (or .env) separately in protected storage. Restoring encrypted credentials requires the backup’s current encryption key and any previous keys then active.

Restore only into an empty or intentionally replaced database:

podman compose -f podman-compose.yaml exec -T postgres \
  sh -c 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"' < team_server.sql

Use the database provider’s backup procedure when PostgreSQL is external.

Follow Maintenance and Recovery for archive verification, restore sequencing, and post-restore checks.

Uninstall

Stop Compose services while preserving their database volume:

podman compose -f podman-compose.yaml down

For a direct container, run podman stop --time 60 endura-team-server, then podman rm endura-team-server. For Quadlet, stop the service and remove its source definition:

systemctl --user stop endura-team-server.service
rm ~/.config/containers/systemd/endura-team-server.container
systemctl --user daemon-reload

Deleting the Compose volume is permanent

podman compose -f podman-compose.yaml down --volumes deletes PostgreSQL and all Team Server data. Back up the database and confirm the project directory before running it.

Troubleshooting

Container Does Not Start

podman compose -f podman-compose.yaml config
podman compose -f podman-compose.yaml logs endura-team-server

Check the configuration mount, referenced environment variables, credential-encryption key, and the two JWT secrets. An unavailable previous key or a credential-authentication failure also prevents startup.

Database Connection Fails

podman compose -f podman-compose.yaml logs postgres
podman compose -f podman-compose.yaml exec postgres \
  sh -c 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

Compose uses the database host postgres. For an external database, confirm DNS, port 5432, TLS, and credentials from the Team Server container.

Rootless Service Stops After Logout

loginctl show-user "$USER" -p Linger
systemctl --user status endura-team-server.service

Enable lingering and use Quadlet for a persistent rootless direct deployment.

SELinux Denies a Mount

Use :Z on private bind mounts and review denials with ausearch -m AVC -ts recent.

Internal OIDC or Integration Endpoint Is Blocked

Configure settings.http.allowed_networks and, when the identity provider or integration endpoint uses an internal CA, settings.http.ca_certificates. See Outbound HTTP Policy.

For additional help, use the Endura contact page.