Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using heroku stack images for local development #56

Closed
fabswt opened this issue May 16, 2017 · 19 comments
Closed

Using heroku stack images for local development #56

fabswt opened this issue May 16, 2017 · 19 comments

Comments

@fabswt
Copy link

fabswt commented May 16, 2017

Hi,

I'd like to run heroku locally to ease development, i.e.: so as to have the same environment for dev as heroku provides for staging and prod.

I'd like to use the heroku Docker images for that. This way, I wouldn't have to install every requirement manually in OS X, and would be able to juggle between different versions with the same ease as on heroku.

It's not clear yet how I can use those images.

I was able to pull the image with docker pull heroku/heroku:16, then start it with docker run -i -t heroku/heroku:16 /bin/bash.

However, I'm a bit confused as to what to do next. Could you provide some guidelines? e.g.:

How do I (Can I?) configure heroku to deploy locally to the local Docker heroku container, instead of the staging or production environments?

@dmathieu
Copy link

Hello there,

When using Docker, you don't need to use the base image we provide (though you absolutely can). You can use any base image you wish.

Docker allows you to set a base image in your Dockerfile with the FROM instruction:

FROM heroku/heroku:16

You can look at our Alpine Hello World app for a very simple example of a Dockerfile.

How do I (Can I?) configure heroku to deploy locally to the local Docker heroku container, instead of the staging or production environments?

There might be a misunderstanding here. When you use your container locally, there is no Heroku involved. You just need to build the container and start it, like any other Docker one.
Only when you wish to push it to our registry will you need to start worrying about us, as the docker push needs to push to registry.heroku.com.
Our registry CLI plugin will handle that for you see the doc.

Note that the big difference between deploying with our container registry and the standard way of using GIT push, GitHub Sync or our API is that the later will allow us to upgrade base libraries such as OpenSSL without the need for a new deployment from you.
We cannot upgrade the base image of your docker container without you triggering a new deployment.

So while we update base packages when there are security issues, we can't do that for docker containers which means that works falls back to you.

@fabswt
Copy link
Author

fabswt commented May 17, 2017

Thank you, that clarifies it a bit.

So if I get that right that means that, even if I use the heroku/heroku:16 image, I still need to install the requirements myself? e.g.: PHP in the specific version I need, the webserver, etc.

Ideally, I wanted to use the exact same environment as heroku uses in production. e.g.: FPM/FastCGI rather than some other version of PHP; identify and enable the same PHP extensions; identify and install the same version of Apache or nginx…

Do I get that right, or is there a simpler way to reproduce the production environment locally?

P.S.: note I don't mean to deploy Docker to heroku. Rather the opposite: I'm using Docker locally only, to try and reproduce the heroku environment locally.

@edmorley
Copy link
Member

Do I get that right,

That's correct.

What would be awesome would be a variant of heroku/heroku:16 that includes a buildpack loader that then clones/runs any listed buildpacks, to provide an environment that more closely matches production.

@edmorley
Copy link
Member

Though doing so would be hacky (or slow), since the Dockerfile would have no way of knowing which files the buildpacks required (eg runtimes.txt, post_compile, yarn.lock), so would either have to copy all of them in, or require the user to do so and keep the list in sync.

@dmathieu
Copy link

Ed is absolutely right.
When deploying your PHP app, we will execute the php buildpack, which will install all dependencies and make the app ready to be run (not as a docker container though. We use LXC).

Your local environment would need to mimick the execution of the buildpack, which would be tricky today.
This is definitely something on our mind though (not only for reproducing production environments locally, but also to make it easier to test buildpacks).

@fabswt
Copy link
Author

fabswt commented May 17, 2017

Thanks a lot, it's a lot clearer now.

There's gonna be a lot of variability of course, but I'll ask anyway: what do people usually go for in terms of local development workflow then? Would you recommend:

  • Using the heroku/heroku:16 Docker image and installing the dependencies I need.
  • Using a default Ubuntu Docker image and installing the dependencies I need.
  • To figure a way to execute the buildpack locally, maybe using LXC inside of Vagrant.
  • Taking it easy: merely installing in OS X the dependencies I need.

Sorry for the many questions. I was able to find a lot of doc in terms of deploying to Heroku, but not so much in terms of best practices for local development.

@dmathieu
Copy link

Some people run the buildpacks in the container (it's possible, the api is public).
Though I haven't seen many do so.

Even using docker will not be exactly the same thing as on our platform though. You will have the same versions, but we block sudo access and allow only storing files in /app at build time while docker allows storing files anywhere on the system.
Using LXC would bring you closer. But is it really worth the effort? You're gonna spend a lot of time setting up the environment.

For most apps, the abstraction level allows them to run the app locally, with the development environment provided by their machine.
Then, review apps allow testing in an environment much similar to the one there is in production.

The other production similarity you won't be able to achieve easily and which reinforces that idea of not trying to mimick production at 100% locally is that our router will proxy requests to your app, while with docker you will call it directly.

@krainboltgreene
Copy link

So I've gotten pretty far by creating a dockerfile that looks like this:


FROM heroku/heroku:16

ENV APPLICATION /app
ENV RAILS_ENV development
ENV RACK_ENV development
ENV PORT 3002
ENV STACK heroku-16

WORKDIR $APPLICATION

COPY Gemfile $APPLICATION/
COPY Gemfile.lock $APPLICATION/

RUN wget -q -O /heroku-buildpack-ruby-master.zip https://github.com/heroku/heroku-buildpack-ruby/archive/master.zip
RUN unzip -q /heroku-buildpack-ruby-master.zip -d /
RUN /heroku-buildpack-ruby-master/bin/detect $APPLICATION && /heroku-buildpack-ruby-master/bin/compile $APPLICATION/ /tmp

EXPOSE $PORT

CMD ["bin/rails", "server", "puma", "--binding=0.0.0.0"]

One difficulty I've encountered is that nokogiri wants gcc. I figured heroku-16 would come with gcc?

@edmorley
Copy link
Member

edmorley commented Jul 25, 2017

I figured heroku-16 would come with gcc?

heroku/heroku:16-build does, but heroku/heroku:16 intentionally does not. See:
https://devcenter.heroku.com/articles/heroku-16-stack#heroku-16-docker-image

The list of packages in each are here:
https://github.com/heroku/stack-images/blob/afcf9ecfaaa37dadd02950bd18e6d3db16a275fe/heroku-16/bin/heroku-16.sh#L87-L127
https://github.com/heroku/stack-images/blob/afcf9ecfaaa37dadd02950bd18e6d3db16a275fe/heroku-16-build/bin/heroku-16-build.sh#L9-L68

(gcc comes along with the build-essential package)

@krainboltgreene
Copy link

krainboltgreene commented Jul 25, 2017

Once I did that it worked!

Now my blocker is that once I create a container around this build none of the gems exist (including bundler).

EDIT: This might have to do with some fuckery I tried with --deployment.

@tt
Copy link
Member

tt commented Jul 27, 2017

If you take a look in $APPLICATION/.profile.d, you'll find a number of scripts created by the buildpack.

You'll likely have to source these to initialize all required environment variables.

@krainboltgreene
Copy link

krainboltgreene commented Aug 18, 2017

So the only thing I have to figure out now is how to source right path changes.

Has anyone done this in an automated way? If not, I think all I need to do is create & COPY over a ~/.profile line, right?

@krainboltgreene
Copy link

krainboltgreene commented Aug 18, 2017

Oh, and the other thing is that the current nodejs buildpack creates a $BUILD_DIR/.profile.d/nodejs.sh file that adds $HOME/node_modules/.bin to the path, but that directory doesn't exist since:

  1. heroku-16 supposes the directory will be /app not /root (which is the $HOME)
  2. The compile script can take a dynamic $BUILD_DIR as an argument.

What am I supposed to do about that?

@ojacobson
Copy link
Contributor

ojacobson commented Aug 18, 2017

It might be worth creating a user during docker build whose home is /app, and using that user. That's consistent with how Heroku dynos run - we don't run your app as root - so it will also help with reproducibility. Something like

RUN useradd --home-dir /app app
USER app

should do it. It may also be worth chowning /app to app: so that that user has permission to write to the dir.

@tt
Copy link
Member

tt commented Sep 6, 2017

I'm closing this issue due to inactivity and as it doesn't seem there's an action for us to take. Please reopen if I'm mistaken.

@tt tt closed this as completed Sep 6, 2017
@edmorley
Copy link
Member

edmorley commented Nov 30, 2017

If it helps for anyone else who comes across this issue looking for how to replicate heroku's dual-image pattern using Docker multi-stage builds... the following is what languages: - ruby produces when using the new heroku.yml build manifest developer preview:

FROM heroku/heroku:16-build as build
COPY . /app
WORKDIR /app
RUN mkdir -p /tmp/buildpack/ruby /tmp/build_cache /tmp/env
RUN curl https://buildpack-registry.s3.amazonaws.com/buildpacks/heroku/ruby.tgz | tar --warning=none -xz -C /tmp/buildpack/ruby
RUN STACK=heroku-16 /tmp/buildpack/ruby/bin/compile /app /tmp/build_cache /tmp/env

FROM heroku/heroku:16
COPY --from=build /app /app
ENV HOME /app
WORKDIR /app
RUN useradd -m heroku
USER heroku
CMD bundle exec puma -C config/puma.rb

@partounian
Copy link

Anyone know how to get the above working with jruby? The download timeouts, but the link seems to work fine when I click it manually.

@jcw-
Copy link

jcw- commented Sep 8, 2019

I grabbed the latest multi-stage build steps for ruby like this:

Generate a rails app

rails new rails-hello-world -T -d postgresql
cd rails-hello-world
git init

Add heroku.yml

build:
  languages:
    - ruby
run:
  web: puma

Commit changes

Create heroku app and deploy (watch output for commands)

heroku create --manifest
heroku stack:set container
git push heroku master

Output

FROM heroku/heroku:18-build as build
COPY . /app
WORKDIR /app
RUN mkdir -p /tmp/buildpack/ruby /tmp/build_cache /tmp/env
RUN curl https://buildpack-registry.s3.amazonaws.com/buildpacks/heroku/ruby.tgz | tar --warning=none -xz -C /tmp/buildpack/ruby
RUN STACK=heroku-18 /tmp/buildpack/ruby/bin/compile /app /tmp/build_cache /tmp/env

FROM heroku/heroku:18
COPY --from=build /app /app
ENV HOME /app
WORKDIR /app
RUN useradd -m heroku
USER heroku
ENTRYPOINT ["/bin/bash", "-l", "-c"]

@partounian
Copy link

As I'm finally becoming a decent software engineer, I'm able to contribute back to the community. I have made significant improvements to the performance of using Heroku's stacks locally.

Well almost. This is almost done, but I can't get the profile.d files to properly source.

First, make sure to set these in your shell config.

export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

Dockerfile

# syntax = docker/dockerfile:1.0-experimental
# https://github.com/heroku/stack-images/issues/56#issuecomment-529177989
FROM heroku/heroku:20-build as build
RUN mkdir -p /tmp/buildpack/ruby /tmp/build_cache /tmp/env
ADD https://buildpack-registry.s3.amazonaws.com/buildpacks/heroku/ruby.tgz ruby.tgz
RUN tar --warning=none -xzf ruby.tgz -C /tmp/buildpack/ruby
ENV  BUNDLE_WITHOUT=""
ENV  BUNDLE_DEPLOYMENT=0 # this doesn't overwrite the current position :(
WORKDIR /app
COPY Gemfile .
COPY Gemfile.lock .
RUN --mount=type=cache,target=/tmp/build_cache STACK=heroku-20 /tmp/buildpack/ruby/bin/compile /app /tmp/build_cache /tmp/env

FROM heroku/heroku:20

ENV HOME /app
WORKDIR /app
RUN useradd -m heroku
USER heroku
COPY --from=build /app /app
COPY . /app

SHELL ["/bin/bash", "-l", "-c", "source /app/.profile.d/ruby.sh"]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants