Merge branch 'v30'

This commit is contained in:
evanpelle
2026-04-01 20:03:39 -07:00
14 changed files with 58 additions and 155 deletions
+3 -3
View File
@@ -126,8 +126,6 @@ jobs:
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
- name: 🚢 Deploy - name: 🚢 Deploy
env: env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
GHCR_REPO: ${{ vars.GHCR_REPO }} GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }} GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
ENV: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }} ENV: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }}
@@ -147,10 +145,12 @@ jobs:
echo "Deployment created in ${SECONDS} seconds" >> $GITHUB_STEP_SUMMARY echo "Deployment created in ${SECONDS} seconds" >> $GITHUB_STEP_SUMMARY
echo "::endgroup::" echo "::endgroup::"
- name: ⏳ Wait for deployment to start - name: ⏳ Wait for deployment to start
env:
API_KEY: ${{ secrets.API_KEY }}
run: | run: |
echo "::group::Wait for deployment to start" echo "::group::Wait for deployment to start"
set -euxo pipefail set -euxo pipefail
while [ "$(curl -s https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do while [ "$(curl -s -H "X-API-Key: ${API_KEY}" https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do
if [ "$SECONDS" -ge 300 ]; then if [ "$SECONDS" -ge 300 ]; then
echo "Timeout: deployment did not start within 5 minutes" echo "Timeout: deployment did not start within 5 minutes"
exit 1 exit 1
+8 -12
View File
@@ -64,8 +64,6 @@ jobs:
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
- name: 🚀 Deploy image - name: 🚀 Deploy image
env: env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
GHCR_REPO: openfront-prod GHCR_REPO: openfront-prod
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }} GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }} DOMAIN: ${{ vars.DOMAIN }}
@@ -82,10 +80,11 @@ jobs:
- name: ⏳ Wait for deployment to start - name: ⏳ Wait for deployment to start
env: env:
FQDN: alpha.${{ vars.DOMAIN }} FQDN: alpha.${{ vars.DOMAIN }}
API_KEY: ${{ secrets.API_KEY }}
run: | run: |
echo "::group::Wait for deployment to start" echo "::group::Wait for deployment to start"
set -euxo pipefail set -euxo pipefail
while [ "$(curl -s https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do while [ "$(curl -s -H "X-API-Key: ${API_KEY}" https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do
if [ "$SECONDS" -ge 300 ]; then if [ "$SECONDS" -ge 300 ]; then
echo "Timeout: deployment did not start within 5 minutes" echo "Timeout: deployment did not start within 5 minutes"
exit 1 exit 1
@@ -115,8 +114,6 @@ jobs:
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
- name: 🚀 Deploy image - name: 🚀 Deploy image
env: env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
GHCR_REPO: ${{ vars.GHCR_REPO }} GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }} GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }} DOMAIN: ${{ vars.DOMAIN }}
@@ -133,10 +130,11 @@ jobs:
- name: ⏳ Wait for deployment to start - name: ⏳ Wait for deployment to start
env: env:
FQDN: beta.${{ vars.DOMAIN }} FQDN: beta.${{ vars.DOMAIN }}
API_KEY: ${{ secrets.API_KEY }}
run: | run: |
echo "::group::Wait for deployment to start" echo "::group::Wait for deployment to start"
set -euxo pipefail set -euxo pipefail
while [ "$(curl -s https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do while [ "$(curl -s -H "X-API-Key: ${API_KEY}" https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do
if [ "$SECONDS" -ge 300 ]; then if [ "$SECONDS" -ge 300 ]; then
echo "Timeout: deployment did not start within 5 minutes" echo "Timeout: deployment did not start within 5 minutes"
exit 1 exit 1
@@ -166,8 +164,6 @@ jobs:
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
- name: 🚀 Deploy image - name: 🚀 Deploy image
env: env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
GHCR_REPO: ${{ vars.GHCR_REPO }} GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }} GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }} DOMAIN: ${{ vars.DOMAIN }}
@@ -184,10 +180,11 @@ jobs:
- name: ⏳ Wait for deployment to start - name: ⏳ Wait for deployment to start
env: env:
FQDN: blue.${{ vars.DOMAIN }} FQDN: blue.${{ vars.DOMAIN }}
API_KEY: ${{ secrets.API_KEY }}
run: | run: |
echo "::group::Wait for deployment to start" echo "::group::Wait for deployment to start"
set -euxo pipefail set -euxo pipefail
while [ "$(curl -s https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do while [ "$(curl -s -H "X-API-Key: ${API_KEY}" https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do
if [ "$SECONDS" -ge 300 ]; then if [ "$SECONDS" -ge 300 ]; then
echo "Timeout: deployment did not start within 5 minutes" echo "Timeout: deployment did not start within 5 minutes"
exit 1 exit 1
@@ -217,8 +214,6 @@ jobs:
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
- name: 🚀 Deploy image - name: 🚀 Deploy image
env: env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
GHCR_REPO: ${{ vars.GHCR_REPO }} GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }} GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }} DOMAIN: ${{ vars.DOMAIN }}
@@ -235,10 +230,11 @@ jobs:
- name: ⏳ Wait for deployment to start - name: ⏳ Wait for deployment to start
env: env:
FQDN: green.${{ vars.DOMAIN }} FQDN: green.${{ vars.DOMAIN }}
API_KEY: ${{ secrets.API_KEY }}
run: | run: |
echo "::group::Wait for deployment to start" echo "::group::Wait for deployment to start"
set -euxo pipefail set -euxo pipefail
while [ "$(curl -s https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do while [ "$(curl -s -H "X-API-Key: ${API_KEY}" https://${FQDN}/commit.txt)" != "${GITHUB_SHA}" ]; do
if [ "$SECONDS" -ge 300 ]; then if [ "$SECONDS" -ge 300 ]; then
echo "Timeout: deployment did not start within 5 minutes" echo "Timeout: deployment did not start within 5 minutes"
exit 1 exit 1
+10 -18
View File
@@ -38,24 +38,14 @@ FROM base
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
nginx \ nginx \
curl \ curl \
jq \
wget \ wget \
supervisor \ supervisor \
apache2-utils \ apache2-utils \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb > cloudflared.deb \
&& dpkg -i cloudflared.deb \
&& rm cloudflared.deb
# Update worker_connections in nginx.conf # Update worker_connections in nginx.conf
RUN sed -i 's/worker_connections [0-9]*/worker_connections 8192/' /etc/nginx/nginx.conf RUN sed -i 's/worker_connections [0-9]*/worker_connections 8192/' /etc/nginx/nginx.conf
# Create cloudflared directory with proper permissions
RUN mkdir -p /etc/cloudflared && \
chown -R node:node /etc/cloudflared && \
chmod -R 755 /etc/cloudflared
# Setup supervisor configuration # Setup supervisor configuration
RUN mkdir -p /var/log/supervisor RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
@@ -64,10 +54,6 @@ COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -f /etc/nginx/sites-enabled/default RUN rm -f /etc/nginx/sites-enabled/default
# Copy and make executable the startup script
COPY startup.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/startup.sh
# Copy production node_modules from prod-deps stage (cached separately from build) # Copy production node_modules from prod-deps stage (cached separately from build)
COPY --from=prod-deps /usr/src/app/node_modules ./node_modules COPY --from=prod-deps /usr/src/app/node_modules ./node_modules
COPY package*.json ./ COPY package*.json ./
@@ -87,8 +73,14 @@ ARG GIT_COMMIT=unknown
RUN echo "$GIT_COMMIT" > static/commit.txt RUN echo "$GIT_COMMIT" > static/commit.txt
ENV GIT_COMMIT="$GIT_COMMIT" ENV GIT_COMMIT="$GIT_COMMIT"
ENV CF_CONFIG_PATH=/etc/cloudflared/config.yml
ENV CF_CREDS_PATH=/etc/cloudflared/creds.json
# Use the startup script as the entrypoint RUN <<'EOF' tee /usr/local/bin/start.sh
ENTRYPOINT ["/usr/local/bin/startup.sh"] #!/bin/sh
if [ "$DOMAIN" = openfront.dev ] && [ "$SUBDOMAIN" != main ]; then
exec timeout 18h /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi
EOF
RUN chmod +x /usr/local/bin/start.sh
ENTRYPOINT ["/usr/local/bin/start.sh"]
-2
View File
@@ -134,8 +134,6 @@ ENV=$ENV
HOST=$HOST HOST=$HOST
GHCR_IMAGE=$GHCR_IMAGE GHCR_IMAGE=$GHCR_IMAGE
GHCR_TOKEN=$GHCR_TOKEN GHCR_TOKEN=$GHCR_TOKEN
CF_ACCOUNT_ID=$CF_ACCOUNT_ID
CF_API_TOKEN=$CF_API_TOKEN
TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
API_KEY=$API_KEY API_KEY=$API_KEY
DOMAIN=$DOMAIN DOMAIN=$DOMAIN
-3
View File
@@ -6,9 +6,6 @@ GHCR_USERNAME=username
GHCR_REPO=your-repo-name GHCR_REPO=your-repo-name
GHCR_TOKEN=your_docker_token_here GHCR_TOKEN=your_docker_token_here
# Cloudflare Configuration
CF_ACCOUNT_ID=your_cloudflare_account_id
CF_API_TOKEN=your_cloudflare_api_token
DOMAIN=your-domain.com DOMAIN=your-domain.com
# API Key # API Key
+7 -1
View File
@@ -12,7 +12,13 @@ proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=API_CACHE:10m inactiv
server { server {
listen 80 default_server; listen 80 default_server;
# Large cookie support
large_client_header_buffers 4 32k;
proxy_buffer_size 32k;
proxy_busy_buffers_size 48k;
proxy_buffers 4 32k;
# Logging # Logging
access_log /var/log/nginx/access.log; access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log; error_log /var/log/nginx/error.log;
@@ -420,18 +420,19 @@ function createMenuElements(
: !BuildableAttacks.has(item.unitType)), : !BuildableAttacks.has(item.unitType)),
) )
.map((item: BuildItemDisplay) => { .map((item: BuildItemDisplay) => {
const canBuildOrUpgrade = params.buildMenu.canBuildOrUpgrade(item);
return { return {
id: `${elementIdPrefix}_${item.unitType}`, id: `${elementIdPrefix}_${item.unitType}`,
name: item.key name: item.key
? item.key.replace("unit_type.", "") ? item.key.replace("unit_type.", "")
: item.unitType.toString(), : item.unitType.toString(),
disabled: () => !canBuildOrUpgrade, disabled: (p: MenuElementParams) =>
color: canBuildOrUpgrade !p.buildMenu.canBuildOrUpgrade(item),
? filterType === "attack" color: (p: MenuElementParams) =>
? COLORS.attack p.buildMenu.canBuildOrUpgrade(item)
: COLORS.building ? filterType === "attack"
: undefined, ? COLORS.attack
: COLORS.building
: COLORS.building,
icon: item.icon, icon: item.icon,
tooltipItems: [ tooltipItems: [
{ text: translateText(item.key ?? ""), className: "title" }, { text: translateText(item.key ?? ""), className: "title" },
@@ -456,7 +457,7 @@ function createMenuElements(
if (buildableUnit === undefined) { if (buildableUnit === undefined) {
return; return;
} }
if (canBuildOrUpgrade) { if (params.buildMenu.canBuildOrUpgrade(item)) {
params.buildMenu.sendBuildOrUpgrade(buildableUnit, params.tile); params.buildMenu.sendBuildOrUpgrade(buildableUnit, params.tile);
} }
params.closeMenu(); params.closeMenu();
+1
View File
@@ -59,6 +59,7 @@ export async function readGameRecord(
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"x-api-key": config.apiKey(),
}, },
}); });
const record = await response.json(); const record = await response.json();
+3
View File
@@ -55,6 +55,9 @@ export function registerGamePreviewRoute(opts: {
const apiDomain = config.jwtIssuer(); const apiDomain = config.jwtIssuer();
const encodedID = encodeURIComponent(gameID); const encodedID = encodeURIComponent(gameID);
const response = await fetch(`${apiDomain}/game/${encodedID}`, { const response = await fetch(`${apiDomain}/game/${encodedID}`, {
headers: {
"x-api-key": config.apiKey(),
},
signal: controller.signal, signal: controller.signal,
}); });
if (!response.ok) return null; if (!response.ok) return null;
+1
View File
@@ -74,6 +74,7 @@ export async function getUserMe(
const response = await fetch(config.jwtIssuer() + "/users/@me", { const response = await fetch(config.jwtIssuer() + "/users/@me", {
headers: { headers: {
authorization: `Bearer ${token}`, authorization: `Bearer ${token}`,
"x-api-key": config.apiKey(),
}, },
}); });
if (response.status !== 200) { if (response.status !== 200) {
-92
View File
@@ -1,92 +0,0 @@
#!/bin/bash
set -e
# Check if required environment variables are set
if [ -z "$CF_API_TOKEN" ] || [ -z "$CF_ACCOUNT_ID" ] || [ -z "$SUBDOMAIN" ] || [ -z "$DOMAIN" ]; then
echo "Error: Required environment variables not set"
echo "Please set CF_API_TOKEN, CF_ACCOUNT_ID, SUBDOMAIN, and DOMAIN"
exit 1
fi
# Generate a unique tunnel name using timestamp
TIMESTAMP=$(date +%Y%m%d%H%M%S)
TUNNEL_NAME="${SUBDOMAIN}-tunnel-${TIMESTAMP}"
echo "Using unique tunnel name: ${TUNNEL_NAME}"
# Create a new tunnel
echo "Creating Cloudflare tunnel for subdomain ${SUBDOMAIN}..."
TUNNEL_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"name\":\"${TUNNEL_NAME}\"}")
# Extract tunnel ID and token
TUNNEL_ID=$(echo $TUNNEL_RESPONSE | jq -r '.result.id')
TUNNEL_TOKEN=$(echo $TUNNEL_RESPONSE | jq -r '.result.token')
if [ -z "$TUNNEL_ID" ] || [ "$TUNNEL_ID" == "null" ]; then
echo "Failed to create tunnel"
echo $TUNNEL_RESPONSE
exit 1
fi
echo "Tunnel created with ID: ${TUNNEL_ID}"
# Configure the tunnel with hostname
echo "Configuring tunnel to point to tunnel-${SUBDOMAIN}.${DOMAIN}..."
curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}/configurations" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"config\":{\"ingress\":[{\"hostname\":\"tunnel-${SUBDOMAIN}.${DOMAIN}\",\"service\":\"http://localhost:80\"},{\"service\":\"http_status:404\"}]}}"
# Update DNS record to point to the new tunnel
echo "Updating DNS record to point to the new tunnel..."
# First check if DNS record exists
DNS_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
ZONE_ID=$(echo $DNS_RECORDS | jq -r '.result[0].id')
if [ -z "$ZONE_ID" ] || [ "$ZONE_ID" == "null" ]; then
echo "Could not find zone ID for domain ${DOMAIN}"
exit 1
fi
# Check for existing record
EXISTING_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=tunnel-${SUBDOMAIN}.${DOMAIN}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
RECORD_ID=$(echo $EXISTING_RECORDS | jq -r '.result[0].id')
# Create or update the DNS record
if [ -z "$RECORD_ID" ] || [ "$RECORD_ID" == "null" ]; then
# Create new record
echo "Creating new DNS record..."
DNS_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"tunnel-${SUBDOMAIN}.${DOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}")
else
# Update existing record
echo "Updating existing DNS record..."
DNS_RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"tunnel-${SUBDOMAIN}.${DOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}")
fi
# Log the tunnel information
echo "Tunnel configuration is set up! Site will be available at: https://tunnel-${SUBDOMAIN}.${DOMAIN}"
# Export the tunnel token for supervisord
export CLOUDFLARE_TUNNEL_TOKEN=${TUNNEL_TOKEN}
# Start supervisord
if [ "$DOMAIN" = openfront.dev ] && [ "$SUBDOMAIN" != main ]; then
exec timeout 18h /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
else
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
fi
-8
View File
@@ -23,11 +23,3 @@ stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0 stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0 stderr_logfile_maxbytes=0
[program:cloudflared]
command=cloudflared tunnel run --token %(ENV_CLOUDFLARE_TUNNEL_TOKEN)s
autostart=true
autorestart=true
user=node
stdout_logfile=/var/log/cloudflared.log
stderr_logfile=/var/log/cloudflared-err.log
@@ -557,7 +557,11 @@ describe("RadialMenuElements", () => {
const subMenu = buildMenuElement.subMenu!(mockParams); const subMenu = buildMenuElement.subMenu!(mockParams);
const cityElement = subMenu.find((item) => item.id === "build_City"); const cityElement = subMenu.find((item) => item.id === "build_City");
expect(cityElement!.color).toBe(COLORS.building); expect(
(cityElement!.color as (params: MenuElementParams) => string)(
mockParams,
),
).toBe(COLORS.building);
}); });
it("should use correct colors for attack elements", () => { it("should use correct colors for attack elements", () => {
@@ -572,16 +576,24 @@ describe("RadialMenuElements", () => {
(item) => item.id === "attack_Atom Bomb", (item) => item.id === "attack_Atom Bomb",
); );
expect(atomBombElement!.color).toBe(COLORS.attack); expect(
(atomBombElement!.color as (params: MenuElementParams) => string)(
mockParams,
),
).toBe(COLORS.attack);
}); });
it("should not set color when element is disabled", () => { it("should use disabled color when element is disabled", () => {
mockBuildMenu.canBuildOrUpgrade = vi.fn(() => false); mockBuildMenu.canBuildOrUpgrade = vi.fn(() => false);
const subMenu = buildMenuElement.subMenu!(mockParams); const subMenu = buildMenuElement.subMenu!(mockParams);
const cityElement = subMenu.find((item) => item.id === "build_City"); const cityElement = subMenu.find((item) => item.id === "build_City");
expect(cityElement!.color).toBeUndefined(); expect(
(cityElement!.color as (params: MenuElementParams) => string)(
mockParams,
),
).toBe(COLORS.building);
}); });
}); });
-4
View File
@@ -62,14 +62,10 @@ echo "Starting new container for ${HOST} environment..."
# Ensure the traefik network exists # Ensure the traefik network exists
docker network create web 2> /dev/null || true docker network create web 2> /dev/null || true
# Remove any existing volume for this container if it exists
docker volume rm "cloudflared-${CONTAINER_NAME}" 2> /dev/null || true
docker run -d \ docker run -d \
--restart="${RESTART}" \ --restart="${RESTART}" \
--env-file "$ENV_FILE" \ --env-file "$ENV_FILE" \
--name "${CONTAINER_NAME}" \ --name "${CONTAINER_NAME}" \
-v "cloudflared-${CONTAINER_NAME}:/etc/cloudflared" \
--network web \ --network web \
--label "traefik.enable=true" \ --label "traefik.enable=true" \
--label "traefik.http.routers.${CONTAINER_NAME}.rule=Host(\`${SUBDOMAIN}.${DOMAIN}\`)" \ --label "traefik.http.routers.${CONTAINER_NAME}.rule=Host(\`${SUBDOMAIN}.${DOMAIN}\`)" \