Search code examples
dockergoogle-cloud-runastrojs

Problem using environment variables with Astro and Google Cloud Run


I am trying to deploy an Astro application as part of a bigger setup in turborepo. The application uses server side rendering, and I am have deployed it to Google Cloud Run in a docker container.

When developing locally I can put this in my env file:

PUBLIC_VALUE="https://example.com/example".

And then when I need to make a request to the url in my application from the client side I can do the following from a .js file somewhere

const BASE_HOST = import.meta.env.PUBLIC_VALUE
export const BASE_URL = BASE_HOST

Then somewhere in a React component i might do this:

const response = await axios.post(${BASE_URL}/signin`, { emailAddress, password });

And it all all works very well when running locally with yarn run dev. However whern deoployed to Google Cloud run in a docker container I can see that the variable is undefined.

I am very used to using GoogleCloud Run. And I have sucesfully gotten my Astro application to run without any problems. Usually in Google Cloud I can set environmental variables in the Google Cloud Console, and then I can fetch them from my code with process.env. I am very used to doing thus with other applications. However when I try fetching my value with the above code "import.meta.env.PUBLIC_VALUE" is undefined at clientside. console.log() with the value clearly shows.

Why is it undefined?

I have included some more info on my setup below:

This is the content of astro.config.


import { defineConfig } from 'astro/config';
import tailwind from "@astrojs/tailwind";
import react from "@astrojs/react";
import 'dotenv/config';
import node from "@astrojs/node";

// https://astro.build/config
export default defineConfig({
  integrations: [tailwind(), react()],
  output: 'server',
  adapter: node({
    mode: "standalone"
  })
});

This is my Dockerfile:


FROM node:18-alpine AS base

# The web Dockerfile is copy-pasted into our main docs at /docs/handbook/deploying-with-docker.
# Make sure you update this Dockerfile, the Dockerfile in the web workspace and copy that over to Dockerfile in the docs.

# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN apk update
RUN yarn global add turbo

FROM base AS builder
# Set working directory
WORKDIR /app
COPY . .
RUN turbo prune --scope=admin-frontend --docker

# Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
WORKDIR /app

# First install dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install

# Build the project and its dependencies
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json

# Uncomment and use build args to enable remote caching
# ARG TURBO_TEAM
# ENV TURBO_TEAM=$TURBO_TEAM

# ARG TURBO_TOKEN
# ENV TURBO_TOKEN=$TURBO_TOKEN

RUN turbo run build --filter=admin-frontend...

FROM base AS runner
WORKDIR /app

# Don't run production as root
RUN addgroup --system --gid 1001 expressjs
RUN adduser --system --uid 1001 expressjs
USER expressjs
COPY --from=installer /app .

ENV HOST=0.0.0.0
ENV PORT=3000
EXPOSE 3000

CMD node ./apps/admin-frontend/dist/server/entry.mjs

As mentioned the project builds and runs smoothly, locally in docker, and on Cloud Run with this setup.


Solution

  • After trying lots of suggestions from Roopa M I ended up just accepting that there must be some weird bug between how Google Cloud Run handles environmental variables and Astro.

    My workaround has been to just never use environmental variables client side. We can do this by fetching them serverside from process.env and then parsing them to the specific component client side. An example of a signup page using this can be seen here:

    
    ---
    import SignUp from "../components/SignUp";
    import { BASE_URL } from '../utils/api';
    ---
    
    <html lang="en">
        <head>
            <meta charset="utf-8" />
            <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
            <title>Sign Up</title>
        </head>
        <body>
            <div>
                <SignUp BASE_URL={BASE_URL}  client:load />
            </div>
        </body>
    </html>
    
    

    As can be seen the variable is just passed from serverside directly in the component.

    Then in api.js its exported like this:

    export const BASE_URL = import.meta.env.PUBLIC_BACKEND_BASE_URL ?? process.env.PUBLIC_BACKEND_BASE_URL
    

    It could be done in one step but having it in its own file makes it easier for me to handle some custom logic regarding the variable.

    I have chosen to prioritise import.meta.env.PUBLIC_BACKEND_BASE_URL, because in other environments such running locally it actually works. So by using the nullish coalescing operator I fetch from import.meta.env if possible. That way in Google Cloud run it will just import from process.env since the other statement will return undefined.